import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Contains a set of tiles.
 * 
 * @author ben
 */
public class Board {
	static public class Location {
		public int i;
		public int j;

		public Location(int i, int j) {
			this.i = i;
			this.j = j;
		}
	};

	Tile pieces[][];
	int width, height;

	LinkedList<GlobalFeature> globalFeatures;

	public Board(int maxX, int maxY) {
		width = maxX;
		height = maxY;
		pieces = new Tile[maxX][];
		for (int i = 0; i < maxX; i++)
			pieces[i] = new Tile[maxY];
		globalFeatures = new LinkedList<GlobalFeature>();
	}

	public Tile get(int i, int j) {
		return pieces[i][j];
	}

	public void set(int i, int j, Tile t) {
		tryTile(i, j, t);
		placeTile();
	}

	/**
	 * Undoable operation.
	 * 
	 * Scenario: - User places a tile (tryTile) - User then sees that no meeple
	 * locations are valid - User then either retracts tile (undoTile) or locks
	 * it in (placeTile)
	 * 
	 */

	private Tile tryTile;
	// store for the tried tile a list of adjacent features for each feature,
	// we do this to test if a meeple location is valid without actually merging
	// the
	// features...
	private Map<Tile.Feature, List<GlobalFeature>> tryTilesAdjacentFeatures;

	public void tryTile(int i, int j, Tile t) {
		tryTile = t;
		tryTile.i = i;
		tryTile.j = j;
		tryTile.board = this;
		// add it to the board, but can be removed still...
		pieces[t.i][t.j] = tryTile;
		tryTile.computeWorldCoordinates();

		tryTilesAdjacentFeatures = new HashMap<Tile.Feature, List<GlobalFeature>>();

		// compute all the valid meeple locations
		for (Tile.Feature f : tryTile.features) {
			List<GlobalFeature> adjacentFeatures = new LinkedList<GlobalFeature>();

			// for each direction, NESW
			for (int dir = 0; dir < 4; dir++) {
				// get relative direction
				int rdir = (dir + t.getRotation()) % 4;
				// Debug.out.printf("dir %d, rdir %d\n", dir, rdir);

				// get the adjacent tile
				Tile adjt = getAdjacentTile(t, dir);

				if (adjt != null) {
					// a tag is a number that refers to the edge-slot that the
					// feature points to
					if (f.connectedTo[rdir] != null) {
						for (int tag : f.connectedTo[rdir]) {
							// each tag corresponds to a tag of an adjacent tile
							int otag = t.edges[rdir].length - 1 - tag;

							Tile.Feature adjf = adjt.getFeatureConnectedTo(Tile
									.oppositeEdge(dir), otag);
							// Debug.out.println();
							assert (adjf != null);
							assert (adjf.type == f.type);
							GlobalFeature feat = getGlobalFeatureThatContainsFeature(adjf);
							if (feat != null) {
								if (!adjacentFeatures.contains(feat)) {
									adjacentFeatures.add(feat);
									// globalFeatures.remove(feat);
								}
							}
						}
					}
				}
			}

			tryTilesAdjacentFeatures.put(f, adjacentFeatures);
		}

	}

	public void undoTile() {
		remove(tryTile);
		tryTile = null;
		tryTilesAdjacentFeatures = null;
	}

	public void placeTile() {
		for (Tile.Feature f : tryTile.features) {
			// now create a new globalfeature that contains this feature
			GlobalFeature gf = new GlobalFeature(f.type);
			gf.addFeature(f);
			for (GlobalFeature agf : tryTilesAdjacentFeatures.get(f)) {
				globalFeatures.remove(agf);
				gf.mergeWith(agf);
			}
			globalFeatures.add(gf);
		}
	}

	public boolean tryMeepleLocation(Tile.Feature f) {
		// if none of the adjacent global features have meeples then its okay to
		// place a meeple here...
		for (GlobalFeature agf : tryTilesAdjacentFeatures.get(f)) {
			if (agf.isOccupied())
				return false;
		}
		return true;
	}

	public Tile getAdjacentTile(Tile t, int dir) {
		int ni = t.i, nj = t.j;
		if (dir == Tile.N)
			nj -= 1;
		else if (dir == Tile.S)
			nj += 1;
		else if (dir == Tile.E)
			ni += 1;
		else if (dir == Tile.W)
			ni -= 1;

		if (isValidCoordinate(ni, nj))
			return get(ni, nj);
		else
			return null;
	}

	// get a tile including non-adjacent (e.g, NE) tiles
	public Tile getNeighbourTile(Tile t, int dir) {
		assert (dir >= 0 && dir < 8);
		int ni = t.i, nj = t.j;
		if (dir == Tile.N || dir == Tile.NE || dir == Tile.NW)
			nj -= 1;
		else if (dir == Tile.S || dir == Tile.SE || dir == Tile.SW)
			nj += 1;

		if (dir == Tile.E || dir == Tile.NE || dir == Tile.SE)
			ni += 1;
		else if (dir == Tile.W || dir == Tile.NW || dir == Tile.SW)
			ni -= 1;

		if (isValidCoordinate(ni, nj))
			return get(ni, nj);
		else
			return null;
	}

	public GlobalFeature getGlobalFeatureThatContainsFeature(Tile.Feature f) {
		return f.global;
		/*
		 * for (GlobalFeature gf: globalFeatures) { if (gf.features.contains(f))
		 * return gf; } return null;
		 */
	}

	public void remove(Tile t) {
		pieces[t.i][t.j] = null;
	}

	// / is (si,sj) on the board?
	public boolean isValidCoordinate(int si, int sj) {
		return si >= 0 && si < width && sj >= 0 && sj < height;
	}

	/**
	 * Checks to see if tile t can be placed at location i,j
	 */
	public boolean isValidLocation(int i, int j, Tile t) {
		if (get(i, j) == null) {
			// check each tile around this position for compatibility
			boolean onetile = false; // =true if there is one adjacent tile
			if (j > 0) {
				Tile above = get(i, j - 1);
				if (above != null && !above.canJoin(Tile.S, t))
					return false;

				onetile |= (above != null);
			}
			// check each tile around this position for compatibility
			if (j < (height - 1)) {
				Tile below = get(i, j + 1);
				if (below != null && !below.canJoin(Tile.N, t))
					return false;

				onetile |= (below != null);
			}
			// check each tile around this position for compatibility
			if (i > 0) {
				Tile left = get(i - 1, j);
				if (left != null && !left.canJoin(Tile.E, t))
					return false;

				onetile |= (left != null);
			}
			// check each tile around this position for compatibility
			if (i < (width - 1)) {
				Tile right = get(i + 1, j);
				if (right != null && !right.canJoin(Tile.W, t))
					return false;

				onetile |= (right != null);
			}

			return onetile;
		} else
			return false;
	}

	// DEPRECATED, use set, or tryTile,placeTile
	public void old_set(int i, int j, Tile t) {
		t.board = this;
		t.i = i;
		t.j = j;
		pieces[i][j] = t;

		// ask t to cache its world coordinates
		t.computeWorldCoordinates();

		// update the global features

		// algorithm:
		// for each feature in the new tile:
		// collect all globalfeatures connected to it
		// merge them and add this feature
		// check the global feature for completion
		// Debug.out.println("===============");
		Debug.out.println("Adding New Tile");
		// Debug.out.println("===============");
		for (Tile.Feature f : t.features) {
			/* print out this tile's feature set */
			/*
			 * Debug.out.printf("Feature %s, connectedTo[",
			 * Tile.typeToString(f.type)); for(int ii=0;ii<4;ii++) { int tags[]
			 * = f.connectedTo[ii]; if (tags==null) Debug.out.print("[],"); else
			 * { Debug.out.print("["); for(int tag: tags)
			 * Debug.out.printf("%d,", tag); Debug.out.print("],"); } }
			 * Debug.out.println();
			 */

			List<GlobalFeature> adjacentFeatures = new LinkedList<GlobalFeature>();

			// for each direction, NESW
			for (int dir = 0; dir < 4; dir++) {
				// get relative direction
				int rdir = (dir + t.getRotation()) % 4;
				// Debug.out.printf("dir %d, rdir %d\n", dir, rdir);

				// get the adjacent tile
				Tile adjt = getAdjacentTile(t, dir);
				if (adjt != null) {
					// a tag is a number that refers to the edge-slot that the
					// feature points to
					if (f.connectedTo[rdir] != null) {
						for (int tag : f.connectedTo[rdir]) {
							// each tag corresponds to a tag of an adjacent tile
							int otag = t.edges[rdir].length - 1 - tag;
							/*
							 * Debug.out.println();
							 * Debug.out.printf("adir %d, tag %d, otag %d",
							 * rdir, tag, otag); Debug.out.println();
							 */

							Tile.Feature adjf = adjt.getFeatureConnectedTo(Tile
									.oppositeEdge(dir), otag);
							// Debug.out.println();
							assert (adjf != null);
							assert (adjf.type == f.type);
							GlobalFeature feat = getGlobalFeatureThatContainsFeature(adjf);
							if (feat != null) {
								if (!adjacentFeatures.contains(feat)) {
									adjacentFeatures.add(feat);
									globalFeatures.remove(feat);
								}
							}
						}
					}
				}
			}

			// now create a new globalfeature that contains this feature
			GlobalFeature gf = new GlobalFeature(f.type);
			gf.addFeature(f);
			for (GlobalFeature agf : adjacentFeatures) {
				gf.mergeWith(agf);
			}
			globalFeatures.add(gf);
		}
	}

}
